Skip to content

feat: rate limiting + suspicious link filtering (#39)#93

Merged
BillChirico merged 6 commits intomainfrom
feat/rate-limit-link-filter
Feb 26, 2026
Merged

feat: rate limiting + suspicious link filtering (#39)#93
BillChirico merged 6 commits intomainfrom
feat/rate-limit-link-filter

Conversation

@BillChirico
Copy link
Collaborator

Summary

Adds rate limiting and link filtering to the moderation pipeline.

Closes #39

Changes

Rate Limiter (src/modules/rateLimit.js)

  • Sliding window per user/channel (default: 10 msgs / 10s)
  • Actions: delete → warn → temp-mute on 3rd trigger
  • Mod/admin role exemptions
  • Memory-efficient: evicts oldest 10% at 10k tracked users

Link Filter (src/modules/linkFilter.js)

  • Configurable domain blocklist
  • Built-in phishing detection (.xyz scam patterns, fake discord/steam domains)
  • Delete + mod channel alert with embed

Event Pipeline — wired before existing spam check:
rate limit → link filter → spam patterns → AI triage

Config

{
  "moderation": {
    "rateLimit": { "enabled": true, "maxMessages": 10, "windowSeconds": 10 },
    "linkFilter": { "enabled": true, "blockedDomains": ["evil.com"] }
  }
}

Testing

73 test files, 1520 tests passing (47 new: 13 rate limit + 34 link filter)

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 26, 2026

Warning

Rate limit exceeded

@BillChirico has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 8 minutes and 21 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between b761492 and e52101b.

📒 Files selected for processing (23)
  • src/api/routes/health.js
  • src/api/ws/logStream.js
  • src/db.js
  • src/index.js
  • src/logger.js
  • src/modules/events.js
  • src/modules/linkFilter.js
  • src/modules/rateLimit.js
  • src/modules/triage.js
  • src/transports/sentry.js
  • src/transports/websocket.js
  • src/utils/modExempt.js
  • tests/api/routes/config.test.js
  • tests/api/routes/guilds.test.js
  • tests/api/utils/configAllowlist.test.js
  • tests/api/utils/validateConfigPatch.test.js
  • tests/api/ws/logStream.test.js
  • tests/modules/linkFilter.test.js
  • tests/modules/rateLimit.test.js
  • tests/modules/triage-prompt.test.js
  • tests/modules/triage-respond.test.js
  • tests/sentry.test.js
  • tests/transports/websocket.test.js
📝 Walkthrough

Walkthrough

This PR implements two new moderation detection layers integrated into the MessageCreate handler: rate limiting tracks messages per user per channel using a sliding window, while link filtering detects suspicious URLs through phishing pattern matching and domain blocklists. Both modules support admin/mod exemptions and moderation alerts.

Changes

Cohort / File(s) Summary
Link Filtering Module
src/modules/linkFilter.js
New module for detecting and blocking suspicious links via URL extraction, phishing pattern matching against TLD patterns, and configurable domain blocklist checks. Includes admin/mod exemption logic and sends embeds to a moderation channel when links are blocked.
Rate Limiting Module
src/modules/rateLimit.js
New module for per-user-per-channel message tracking using a sliding window mechanism. Issues warnings on first offense and applies temporary mutes on repeat violations. Supports admin/mod role exemptions and includes memory management with entry eviction at capacity caps.
MessageCreate Handler Integration
src/modules/events.js
Integrated rate-limiting and link-filter checks into the MessageCreate event handler, executing before existing spam detection logic. Handler returns early if rate limits are exceeded or blocked links are detected, both gated behind moderation.enabled.
Test Coverage
tests/modules/linkFilter.test.js, tests/modules/rateLimit.test.js
Comprehensive test suites covering URL extraction and deduplication, phishing pattern detection, link blocklist matching, rate limit sliding window behavior, per-user-per-channel independence, role-based exemptions, repeat-offender mute escalation, and edge cases with empty messages.

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main addition: rate limiting and link filtering features, directly matching the substantial changes made to the codebase.
Description check ✅ Passed The description is clearly related to the changeset, detailing rate limiting, link filtering, configuration, testing, and event pipeline integration—all present in the code changes.
Linked Issues check ✅ Passed The PR implements all primary coding requirements from issue #39: sliding-window rate limiting with configurable thresholds and exemptions, link filtering with blocklist and phishing detection, mod alerts, message deletion, and memory efficiency.
Out of Scope Changes check ✅ Passed All changes directly support issue #39 requirements: three new modules (rateLimit, linkFilter, events integration) and comprehensive test suites with no extraneous modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 88.24% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/rate-limit-link-filter

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 26, 2026

Note

Unit test generation is a public access feature. Expect some limitations and changes as we gather feedback and continue to improve it.


Generating unit tests... This may take up to 20 minutes.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 26, 2026

✅ Created PR with unit tests: #96

@greptile-apps
Copy link

greptile-apps bot commented Feb 26, 2026

Greptile Summary

This PR successfully implements rate limiting and link filtering for the moderation pipeline, adding two new robust modules that integrate cleanly into the event handler before existing spam detection and AI triage.

Key Changes:

  • Rate Limiter (src/modules/rateLimit.js): Implements sliding window tracking per user/channel with configurable limits (default: 10 msgs/10s). Escalates from message deletion → user warning → temp-mute on repeated violations. Includes memory-efficient eviction strategy (10% of oldest entries when 10k cap reached).
  • Link Filter (src/modules/linkFilter.js): Provides configurable domain blocklist with subdomain matching, plus built-in phishing detection for suspicious TLD patterns (.xyz domains with scam keywords like discord/nitro/steam).
  • Shared Exemption Logic (src/utils/modExempt.js): Consolidates mod/admin permission checks to avoid duplication between modules.
  • Pipeline Integration (src/modules/events.js): Wires both checks into messageCreate handler with isolated error handling, running before spam detection and AI triage.

Testing:
Excellent test coverage with 47 new tests (13 for rate limiting, 34 for link filtering). Tests cover sliding window behavior, exemptions, repeat offender escalation, memory cap eviction, URL extraction, phishing pattern matching, and blocklist logic. All 1520 tests passing.

Code Quality:

  • Follows project conventions (Winston logging, ESM modules, node: protocol, 2-space indentation)
  • Previous review comments addressed (permission checks implemented with PermissionFlagsBits constants)
  • Proper use of safe message utilities (safeSend, safeReply, sanitizeMentions)
  • Clean error handling with fallback behavior

Confidence Score: 5/5

  • This PR is safe to merge with no blocking issues
  • The implementation is well-architected with comprehensive test coverage (47 new tests, 1520 total passing), properly addresses previous review comments, follows all project conventions, and includes appropriate error handling. Both new modules are thoroughly tested and cleanly integrated into the existing event pipeline.
  • No files require special attention

Important Files Changed

Filename Overview
src/modules/rateLimit.js New rate limiting module with sliding window tracking, escalation to temp-mute, and memory-efficient eviction strategy - well-tested and properly integrated
src/modules/linkFilter.js New link filtering module with phishing detection and configurable blocklist - solid regex patterns and proper URL extraction logic
src/utils/modExempt.js New shared utility for mod/admin exemption checking - consolidates permission logic used by rate limiter and link filter
src/modules/events.js Integrated rate limit and link filter checks into message pipeline before spam detection and AI triage - proper error handling with isolated try-catch blocks
tests/modules/rateLimit.test.js Comprehensive test coverage (342 lines) for sliding window, exemptions, repeat offender muting, and memory cap - uses fake timers appropriately
tests/modules/linkFilter.test.js Comprehensive test coverage (353 lines) for URL extraction, phishing pattern detection, blocklist matching, and exemptions

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Message Received] --> B{moderation.enabled?}
    B -->|No| G[Continue to AI Triage]
    B -->|Yes| C[Check Rate Limit]
    C --> D{Rate Limited?}
    D -->|Yes| E[Delete Message]
    E --> F{Repeat Offender?}
    F -->|Yes| F1[Apply Timeout + Alert Mods]
    F -->|No, First Trigger| F2[Warn User]
    F1 --> END[Stop Processing]
    F2 --> END
    D -->|No| H[Check Link Filter]
    H --> I{Blocked Link?}
    I -->|Yes| J[Delete + Alert Mods]
    J --> END
    I -->|No| K[Check Spam Patterns]
    K --> L{Is Spam?}
    L -->|Yes| M[Alert Mods]
    M --> END
    L -->|No| G
    G --> N[AI Triage Evaluation]
Loading

Last reviewed commit: e52101b

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

22 files reviewed, 5 comments

Edit Code Review Agent Settings | Greptile

@claude
Copy link

claude bot commented Feb 26, 2026

Claude finished @BillChirico's task in 3m 13s —— View job


Code Review — PR #93: Rate Limiting + Link Filtering

  • Read AGENTS.md and project conventions
  • Read all new/changed source files
  • Read test files
  • Read existing config.json and related files
  • Analyze issues and post inline comments
  • Post top-level summary and verdict

Summary

The implementation is solid — sliding window rate limiting with progressive escalation, phishing pattern detection, domain blocklist filtering, shared exemption utility, error isolation in the event pipeline, and comprehensive test coverage (including eviction and mixed-case normalization). Most issues from previous reviews have been addressed.

3 remaining issues — all documentation gaps:

🟡 Warnings

  1. config.json — missing rateLimit and linkFilter defaults
    Per AGENTS.md: "Config for your module goes in config.json under a new key." Both modules read from config.moderation.rateLimit and config.moderation.linkFilter, but neither section exists in the default config. Add both (disabled by default) inside the moderation block.

  2. AGENTS.md — missing Key Files entries
    Per AGENTS.md: "Added a new module → update Key Files table." Add entries for src/modules/rateLimit.js, src/modules/linkFilter.js, and src/utils/modExempt.js.

  3. README.md — missing config documentation
    Per AGENTS.md: "if you add a new config section or key, document it in README.md's config reference." Add rows for rateLimit.* and linkFilter.* to the moderation config table.

🔵 Nitpick

  1. src/utils/modExempt.js:23isExempt doesn't check bot owners or ManageGuild
    The existing isModerator() in src/utils/permissions.js exempts bot owners and ManageGuild holders. isExempt doesn't, so bot owners and ManageGuild users will still be rate-limited/link-filtered.
🤖 AI fix prompt — copy and paste to fix all issues
Fix the following documentation issues on branch feat/rate-limit-link-filter in VolvoxLLC/volvox-bot:

1. config.json — Add rateLimit and linkFilter config sections inside the "moderation" block
   (after the "logging" section, before the closing brace on line 81). Use these defaults:
   "rateLimit": { "enabled": false, "maxMessages": 10, "windowSeconds": 10,
     "muteAfterTriggers": 3, "muteWindowSeconds": 300, "muteDurationSeconds": 300 }
   "linkFilter": { "enabled": false, "blockedDomains": [] }

2. AGENTS.md — Add these entries to the Key Files table (after line 33, the moderation.js row):
   | `src/modules/rateLimit.js` | Rate limiting — sliding window per user/channel
     with progressive escalation (delete → warn → temp-mute) |
   | `src/modules/linkFilter.js` | Link filtering — domain blocklist + phishing
     pattern detection with mod alerts |
   | `src/utils/modExempt.js` | Shared mod/admin exemption check used by rate
     limiter and link filter |

3. README.md — Add these rows to the Moderation config table (after line 200):
   | `rateLimit.enabled` | boolean | Enable sliding-window rate limiting |
   | `rateLimit.maxMessages` | number | Max messages per window (default: 10) |
   | `rateLimit.windowSeconds` | number | Sliding window size in seconds (default: 10) |
   | `rateLimit.muteAfterTriggers` | number | Trigger count before temp-mute (default: 3) |
   | `rateLimit.muteWindowSeconds` | number | Window for counting triggers (default: 300) |
   | `rateLimit.muteDurationSeconds` | number | Temp-mute duration in seconds (default: 300) |
   | `linkFilter.enabled` | boolean | Enable suspicious link filtering |
   | `linkFilter.blockedDomains` | string[] | Domain blocklist (e.g. `["evil.com"]`) |

4. (Optional) src/utils/modExempt.js — Consider importing getBotOwnerIds from
   src/utils/permissions.js and adding a bot-owner check, plus a
   PermissionFlagsBits.ManageGuild check, to match the behavior of isModerator().

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 26, 2026
Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issues Found

🟡 Warnings (3)

  1. rateLimit.js:110-113 — Raw message.reply() bypasses safeReply(): Per project conventions, all outgoing messages must use safe wrappers to enforce allowedMentions and sanitize mentions. The warning reply uses raw message.reply().

  2. rateLimit.js:50 / linkFilter.js:81config.permissions.modRoles doesn't exist in config.json: Both modules rely on config.permissions?.modRoles for mod exemptions, but config.json only has permissions.adminRoleId and permissions.moderatorRoleId (singular strings). The mod-role exemption path is effectively dead code. Either add modRoles: [] to config.json or use the existing fields.

  3. Missing rateLimit and linkFilter defaults in config.json: Per AGENTS.md, new modules must have their config section added to config.json. Users can't discover these options without it. Also, AGENTS.md Key Files table and README.md should be updated per documentation requirements.

🔵 Nitpicks (3)

  1. Duplicated isExempt function — Identical logic in both rateLimit.js:43-54 and linkFilter.js:75-87. Should be extracted to a shared utility.

  2. Unsanitized user content in embeds — Both modules embed message.author.tag and message content in alert embeds without sanitization. Low risk (embed fields don't trigger pings) but inconsistent with the project's defense-in-depth pattern.

  3. Redundant moderation.enabled check in events.js — Two separate identical if blocks can be combined.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/modules/events.js`:
- Around line 98-108: Wrap the two moderation pre-checks (the calls to
checkRateLimit(message, guildConfig) and checkLinks(message, guildConfig)
guarded by guildConfig.moderation?.enabled) in a local try/catch so any thrown
error is caught and the listener doesn't reject; in the catch, call your logger
(or processLogger) with context including the function/listener name, the
message id or author, and guild id, then safely continue (or return) without
rethrowing so a single failing check cannot crash the event handler.

In `@src/modules/linkFilter.js`:
- Around line 148-156: Normalize configured blocked domain entries before
matching: when building blockedDomains (used in the link filtering loop that
calls extractUrls and compares hostname to blocked), map lfConfig.blockedDomains
to a normalized list by trimming whitespace, lowercasing, removing a leading
"www." (and any trailing dot), and then use that normalized array in the
matching logic; keep hostname normalization as-is and compare hostname ===
blocked || hostname.endsWith(`.${blocked}`) against the normalizedBlocked list
instead of the raw lfConfig entries.

In `@src/modules/rateLimit.js`:
- Around line 62-63: The mute alert embed currently hard-codes "3 triggers in 5
minutes"; update handleRepeatOffender (and the other embed constructions at the
locations referenced) to build the reason string from the config values instead:
use config.triggerCount and convert the window (e.g., config.windowMinutes or
config.windowMs -> minutes) dynamically and include the muteDurationMs
(formatted to minutes/seconds as appropriate) so the embed reason reflects the
actual config (e.g., `${config.triggerCount} triggers in ${windowMinutes}
minutes`), replacing the hard-coded text wherever it appears (lines around
handleRepeatOffender and the other two embed usages).
- Around line 109-113: In warnUser, don't call message.reply directly; replace
that raw reply with the project's safe message helper (e.g., safeSend or
safeReply) to ensure mention sanitization and allowedMentions enforcement:
update the import/require to pull in the safe helper, call safeSend/safeReply in
place of message.reply with the same composed text, and preserve the existing
catch behavior (returning null) so behavior and error handling remain
consistent; reference the warnUser function and the safeSend/safeReply helper
when making the change.

In `@tests/modules/linkFilter.test.js`:
- Around line 185-249: Add a regression test in the existing "checkLinks —
blocklist matching" suite to verify mixed-case entries in blockedDomains are
matched case-insensitively: create a config via makeConfig({ blockedDomains:
['Evil.COM'] }), a message via makeMessage({ content: 'https://evil.com/path'
}), call checkLinks(msg, config) and assert result.blocked is true (and
optionally expect(msg.delete).toHaveBeenCalledTimes(1)). Place the new it(...)
near the other blocklist tests so it uses the same helpers (checkLinks,
makeConfig, makeMessage) and ensures domain matching is normalized.

In `@tests/modules/rateLimit.test.js`:
- Around line 292-315: The test never triggers eviction because it never exceeds
the module's MAX_TRACKED_USERS; update the test to actually exercise eviction by
either (a) temporarily lowering the module's MAX_TRACKED_USERS (mock or set the
exported constant) before the loop so checkRateLimit is called enough times to
exceed that smaller cap and then assert getTrackedCount() <= that cap, or (b)
modify the rate-limit implementation to accept a cap via config (e.g., add a
maxTrackedUsers option consumed by checkRateLimit) and in the test pass a small
cap so the eviction path runs; target the checkRateLimit, getTrackedCount and
MAX_TRACKED_USERS (or new config option) symbols when making the change.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8ae8d7d and b761492.

📒 Files selected for processing (5)
  • src/modules/events.js
  • src/modules/linkFilter.js
  • src/modules/rateLimit.js
  • tests/modules/linkFilter.test.js
  • tests/modules/rateLimit.test.js
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (4)
**/*.js

📄 CodeRabbit inference engine (AGENTS.md)

**/*.js: Use ESM modules only — use import/export, never require()
Use node: protocol for Node.js builtins (e.g. import { readFileSync } from 'node:fs')
Always use semicolons
Use single quotes for strings
Use 2-space indentation
No TypeScript — use plain JavaScript with JSDoc comments for documentation

Files:

  • src/modules/events.js
  • src/modules/rateLimit.js
  • tests/modules/linkFilter.test.js
  • src/modules/linkFilter.js
  • tests/modules/rateLimit.test.js
src/**/*.js

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.js: Always use Winston for logging — import { info, warn, error } from ../logger.js
NEVER use console.log, console.warn, console.error, or any console.* method in src/ files
Pass structured metadata to Winston logging calls (e.g. info('Message processed', { userId, channelId }))
Use custom error classes from src/utils/errors.js for error handling
Always log errors with context before re-throwing
Use getConfig(guildId?) from src/modules/config.js to read config
Use setConfigValue(path, value, guildId?) from src/modules/config.js to update config at runtime
Use splitMessage() utility for messages exceeding Discord's 2000-character limit
Use safeSend() wrapper for outgoing Discord messages to sanitize mentions and enforce allowedMentions

Files:

  • src/modules/events.js
  • src/modules/rateLimit.js
  • src/modules/linkFilter.js
src/modules/events.js

📄 CodeRabbit inference engine (AGENTS.md)

Register event handlers in src/modules/events.js by calling client.on() with the event name and handler function

Files:

  • src/modules/events.js
tests/**/*.js

📄 CodeRabbit inference engine (AGENTS.md)

Test files must achieve at least 80% code coverage on statements, branches, functions, and lines

Files:

  • tests/modules/linkFilter.test.js
  • tests/modules/rateLimit.test.js
🧠 Learnings (7)
📚 Learning: 2026-02-25T02:39:33.506Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-25T02:39:33.506Z
Learning: Applies to src/**/*.js : Use `safeSend()` wrapper for outgoing Discord messages to sanitize mentions and enforce allowedMentions

Applied to files:

  • src/modules/events.js
  • src/modules/rateLimit.js
  • src/modules/linkFilter.js
📚 Learning: 2026-02-25T02:39:33.506Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-25T02:39:33.506Z
Learning: Applies to src/commands/*mod*.js : Moderation commands must call `checkHierarchy(moderator, target)` before executing mod actions to prevent moderating users with equal or higher roles

Applied to files:

  • src/modules/events.js
  • src/modules/rateLimit.js
📚 Learning: 2026-02-25T02:39:33.506Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-25T02:39:33.506Z
Learning: Applies to src/commands/*mod*.js : Send DM notifications to moderation targets *before* executing kicks/bans (once kicked/banned, users cannot receive DMs from the bot)

Applied to files:

  • src/modules/events.js
📚 Learning: 2026-02-25T02:39:33.506Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-25T02:39:33.506Z
Learning: Applies to src/commands/*mod*.js : Moderation commands must follow the pattern: deferReply → validate inputs → sendDmNotification → execute Discord action → createCase → sendModLogEmbed → checkEscalation

Applied to files:

  • src/modules/events.js
📚 Learning: 2026-02-25T02:39:33.506Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-25T02:39:33.506Z
Learning: Applies to src/**/*.js : Use `splitMessage()` utility for messages exceeding Discord's 2000-character limit

Applied to files:

  • src/modules/rateLimit.js
  • src/modules/linkFilter.js
  • tests/modules/rateLimit.test.js
📚 Learning: 2026-02-25T02:39:33.506Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-25T02:39:33.506Z
Learning: Applies to src/modules/triage.js : Triage tracks at most 100 channels; channels inactive for 30 minutes are evicted from the buffer

Applied to files:

  • src/modules/rateLimit.js
📚 Learning: 2026-02-25T02:39:33.506Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-25T02:39:33.506Z
Learning: Applies to src/commands/*timeout*.js : Discord timeouts max at 28 days; enforce this cap in timeout commands

Applied to files:

  • src/modules/rateLimit.js
  • tests/modules/rateLimit.test.js
🧬 Code graph analysis (5)
src/modules/events.js (2)
src/modules/rateLimit.js (1)
  • checkRateLimit (129-204)
src/modules/linkFilter.js (1)
  • checkLinks (125-172)
src/modules/rateLimit.js (2)
src/logger.js (2)
  • warn (237-239)
  • info (230-232)
src/utils/safeSend.js (1)
  • safeSend (116-123)
tests/modules/linkFilter.test.js (2)
src/modules/linkFilter.js (5)
  • member (76-76)
  • results (37-37)
  • extractUrls (36-54)
  • matchPhishingPattern (61-67)
  • checkLinks (125-172)
src/utils/permissions.js (1)
  • isAdmin (52-71)
src/modules/linkFilter.js (3)
tests/modules/linkFilter.test.js (1)
  • member (29-38)
src/utils/safeSend.js (1)
  • safeSend (116-123)
src/logger.js (1)
  • warn (237-239)
tests/modules/rateLimit.test.js (2)
src/modules/rateLimit.js (5)
  • member (44-44)
  • member (63-63)
  • i (28-28)
  • checkRateLimit (129-204)
  • getTrackedCount (217-219)
src/utils/permissions.js (1)
  • isAdmin (52-71)
🪛 GitHub Actions: CI
src/modules/linkFilter.js

[error] 16-16: lint/complexity/noUselessEscapeInRegex: Unescape the character in URL_REGEX.

🔇 Additional comments (1)
src/modules/linkFilter.js (1)

16-16: Regex escape has already been fixed in the current code.

The URL_REGEX at line 16 already uses the correct form [a-z0-9-] (without useless escaping), so the proposed fix is already applied. The noUselessEscapeInRegex lint rule should not be violated by this code.

Likely an incorrect or invalid review comment.

@greptile-apps
Copy link

greptile-apps bot commented Feb 26, 2026

Additional Comments (3)

config.json
missing rateLimit and linkFilter configuration sections - add these to the moderation config so users know how to configure them:

"rateLimit": {
  "enabled": false,
  "maxMessages": 10,
  "windowSeconds": 10,
  "muteAfterTriggers": 3,
  "muteWindowSeconds": 300,
  "muteDurationSeconds": 300
},
"linkFilter": {
  "enabled": false,
  "blockedDomains": []
}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: config.json
Line: 53-80

Comment:
missing `rateLimit` and `linkFilter` configuration sections - add these to the moderation config so users know how to configure them:
```json
"rateLimit": {
  "enabled": false,
  "maxMessages": 10,
  "windowSeconds": 10,
  "muteAfterTriggers": 3,
  "muteWindowSeconds": 300,
  "muteDurationSeconds": 300
},
"linkFilter": {
  "enabled": false,
  "blockedDomains": []
}
```

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

README.md
document new rateLimit and linkFilter config options in the moderation section - users need to know how to configure these new features

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: README.md
Line: 181-201

Comment:
document new `rateLimit` and `linkFilter` config options in the moderation section - users need to know how to configure these new features

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

AGENTS.md
add the new modules to the Key Files table:

  • src/modules/rateLimit.js — Rate limiting — sliding window per user/channel with progressive escalation (delete → warn → temp-mute)
  • src/modules/linkFilter.js — Link filtering — domain blocklist + phishing pattern detection

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: AGENTS.md
Line: 20-37

Comment:
add the new modules to the Key Files table:
- `src/modules/rateLimit.js` — Rate limiting — sliding window per user/channel with progressive escalation (delete → warn → temp-mute)
- `src/modules/linkFilter.js` — Link filtering — domain blocklist + phishing pattern detection

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary — 6 Warnings, 3 Nitpicks

🟡 Warnings

  1. src/modules/rateLimit.js:111-117 — Raw message.reply() bypasses safeReply()
    Per AGENTS.md, all outgoing messages must use safe wrappers. Import and use safeReply instead.

  2. src/modules/rateLimit.js:50 + src/modules/linkFilter.js:81config.permissions.modRoles doesn't exist in config.json
    Both modules check config.permissions?.modRoles (an array), but config only has adminRoleId / moderatorRoleId (singular strings). The mod-role exemption path is dead code. Consider reusing isModerator() from src/utils/permissions.js.

  3. src/modules/events.js:98-108 — No error isolation around moderation pre-checks
    If checkRateLimit or checkLinks throws, the entire MessageCreate handler rejects. Wrap in try/catch.

  4. src/modules/linkFilter.js:150 — Blocklist domain comparison is case-sensitive
    hostname is lowercased but blockedDomains config entries are not normalized. Evil.COM won't match evil.com.

  5. src/modules/rateLimit.js:98 — Hard-coded "3 triggers in 5 minutes" in mute embed
    These values come from config. Build the reason string dynamically.

  6. config.json + AGENTS.md — Missing config defaults and documentation
    Per AGENTS.md: new modules need config defaults in config.json, entries in the Key Files table, and README.md documentation. Add rateLimit and linkFilter sections to moderation in config.json (disabled by default), update AGENTS.md Key Files table, and document the new config options in README.md.

🔵 Nitpicks

  1. src/modules/rateLimit.js:69 — Inconsistent permission flag usage
    Uses string 'ModerateMembers' instead of PermissionFlagsBits.ModerateMembers.

  2. src/modules/rateLimit.js:43-56 + src/modules/linkFilter.js:75-87 — Duplicated isExempt function
    Identical logic in both files. Extract to a shared utility.

  3. tests/modules/rateLimit.test.js:296-319 — Memory cap test doesn't exercise eviction
    Creates only 50 entries but eviction triggers at 10,000. The eviction code path has zero test coverage.


🤖 AI fix prompt — copy and paste to fix all issues
Fix the following issues on branch feat/rate-limit-link-filter in VolvoxLLC/volvox-bot:

1. src/modules/rateLimit.js:111-117 — Replace `message.reply()` with `safeReply(message, ...)` from `../utils/safeSend.js`. Add `safeReply` to the import on line 9.

2. src/modules/rateLimit.js:50 and src/modules/linkFilter.js:81 — The `isExempt` function checks `config.permissions?.modRoles` which doesn't exist in config.json. Either: (a) add `"modRoles": []` to the `permissions` section of `config.json`, or (b) refactor `isExempt` to use the existing `isModerator()` from `src/utils/permissions.js` which already checks `adminRoleId`, `moderatorRoleId`, and `PermissionFlagsBits.Administrator`.

3. src/modules/events.js:98-108 — Wrap both moderation pre-checks (checkRateLimit + checkLinks) in a single `if (guildConfig.moderation?.enabled)` block with a try/catch. In the catch, call `logError('Moderation pre-check failed', { guildId, channelId, userId, error })`.

4. src/modules/linkFilter.js:150 — Normalize blockedDomains entries: `.map(d => d.toLowerCase().trim()).filter(Boolean)`.

5. src/modules/rateLimit.js:98 — Replace hard-coded "3 triggers in 5 minutes" with values from config: `${muteThreshold} triggers in ${Math.round(muteWindowSeconds / 60)} minute(s)`. Thread muteThreshold and muteWindowSeconds into handleRepeatOffender.

6. src/modules/rateLimit.js:69 — Change `'ModerateMembers'` to `PermissionFlagsBits.ModerateMembers`.

7. Extract the duplicated `isExempt` function from rateLimit.js:43-56 and linkFilter.js:75-87 into a shared utility in src/utils/ and import it in both files.

8. config.json — Add rateLimit and linkFilter config sections inside the "moderation" block (after line 80), both with "enabled": false by default.

9. AGENTS.md — Add these entries to the Key Files table:
   - `src/modules/rateLimit.js` | Rate limiting — sliding window per user/channel with progressive escalation
   - `src/modules/linkFilter.js` | Link filtering — domain blocklist + phishing pattern detection

10. tests/modules/rateLimit.test.js:296-319 — Make the memory cap test actually trigger eviction. Either export a setter for MAX_TRACKED_USERS from rateLimit.js for testing, or create 10,001 unique users in the test to trigger the eviction path, then assert getTrackedCount() <= 10000.

11. tests/modules/linkFilter.test.js — Add a regression test for mixed-case blockedDomains: `makeConfig({ blockedDomains: ['Evil.COM'] })` with message `'https://evil.com/path'` should still block.

- Sliding window per user/channel (default: 10 msgs/10s)
- Config: moderation.rateLimit.{enabled,maxMessages,windowSeconds}
- Actions: delete excess msg, warn user (auto-delete after 10s)
- Repeat offender: temp-mute after 3 triggers in 5 min
- Exempt ADMINISTRATOR permission + modRoles
- Memory cap: evicts 10% when 10,000 tracked users reached
- Exports: checkRateLimit(), clearRateLimitState(), getTrackedCount()
- 13 tests: sliding window, exemptions, mute escalation, memory cap
- Extracts URLs via regex, checks against moderation.linkFilter.blockedDomains
- Subdomain matching (blocklist 'evil.com' catches 'sub.evil.com')
- Phishing patterns: .xyz domains with discord/nitro/free keywords,
  discord-nitro/free-nitro subdomains, steamgift domains
- Actions: delete message, alert mod channel with embed
- Exempt ADMINISTRATOR permission + modRoles
- Exports: checkLinks(), extractUrls(), matchPhishingPattern()
- 34 tests: URL extraction, blocklist, phishing, exemptions, edge cases
…39)

Order: rate limit → link filter → spam patterns → AI triage
Both checks are gated on moderation.enabled and return early on match.
Adds permission guard before calling member.timeout() to prevent
crashes when the bot lacks MODERATE_MEMBERS permission. Updates test
mock to include guild.members.me reference.
- Use safeReply instead of raw message.reply in warnUser (rateLimit.js)
- Normalize blockedDomains entries (lowercase, strip www.) in linkFilter.js
- Wrap checkRateLimit and checkLinks in isolated try/catch in events.js
- Fix memory-cap test to actually exercise eviction via setMaxTrackedUsers
- Replace hard-coded '3 triggers in 5 minutes' with actual config values
- Sanitize message.author.tag and content in embed fields via sanitizeMentions
- Extract shared isExempt to src/utils/modExempt.js; support both modRoles[]
  and adminRoleId/moderatorRoleId config schemas
- Combine duplicate moderation?.enabled blocks in events.js
- Use PermissionFlagsBits.ModerateMembers instead of string literal
- Add mixed-case blockedDomains test in linkFilter.test.js
@BillChirico BillChirico force-pushed the feat/rate-limit-link-filter branch from bfe9b51 to e52101b Compare February 26, 2026 03:35
Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary — 3 Warnings, 1 Nitpick

The code quality is solid — previous review feedback has been largely addressed (safeReply, error isolation, domain normalization, PermissionFlagsBits, dynamic reason text, shared isExempt utility, eviction test coverage). The remaining issues are documentation gaps that violate AGENTS.md requirements.

Warnings

  1. config.json — missing rateLimit and linkFilter config defaults — Per AGENTS.md: 'Config for your module goes in config.json under a new key.' Both modules read from config.moderation.rateLimit and config.moderation.linkFilter, but neither section exists in the default config. Add both sections (disabled by default) inside the moderation block after logging.

  2. AGENTS.md — missing Key Files entries — Per AGENTS.md: 'Added a new module → update Key Files table.' Add entries for src/modules/rateLimit.js, src/modules/linkFilter.js, and src/utils/modExempt.js.

  3. README.md — missing documentation for new config options — Per AGENTS.md: 'if you add a new config section or key, document it in README.md config reference.' The moderation config table (lines 181-201) needs rows for rateLimit.* and linkFilter.* options.

Nitpick

  1. src/utils/modExempt.js:23 — isExempt doesn't check bot owners or ManageGuild — The existing isModerator() in src/utils/permissions.js exempts bot owners and ManageGuild holders. isExempt doesn't, so bot owners and ManageGuild users will still be rate-limited/link-filtered.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Moderation: rate limiting + suspicious link filtering

1 participant